package org.hzz.application.handler.user;

import io.gitee.pkmer.convention.exception.AppCommonException;
import io.gitee.pkmer.convention.exception.request.BadRequestException;
import io.gitee.pkmer.designpattern.strategy.AbstractExecuteStrategy;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.hzz.application.command.user.ReceiveCouponCmd;
import org.hzz.promotions.domain.entity.CouponEntity;
import org.hzz.promotions.domain.entity.ExchangeCodeEntity;
import org.hzz.promotions.domain.entity.UserCouponEntity;
import org.hzz.promotions.domain.service.CouponDomainService;
import org.hzz.promotions.domain.service.ExchangeCodeDomainService;
import org.hzz.promotions.domain.service.UserCouponDomainService;
import org.hzz.promotions.domain.valueobject.CouponReceive;
import org.hzz.promotions.types.enums.code.CodeType;
import org.hzz.promotions.types.enums.coupon.CouponStatus;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Map;

/**
 * @author <a href="mailto:1193094618@qq.com">pkmer</a>
 * <br>
 * <a href = "https://gitee.com/developeros/videos-online">Code Repository</a>
 * At 2024/9/2
 */
@Component
@Slf4j
public class ReceiveCouponCmdHandler implements AbstractExecuteStrategy<ReceiveCouponCmd, Void> {

    @Setter(onMethod_ = @Autowired)
    private CouponDomainService couponDomainService;
    @Setter(onMethod_ = @Autowired)
    private UserCouponDomainService userCouponDomainService;
    @Setter(onMethod_ = @Autowired)
    private ExchangeCodeDomainService codeDomainService;

    @Autowired
    @Lazy
    private ReceiveCouponCmdHandler self;

    @Override
    public String mark() {
        return ReceiveCouponCmd.MARK;
    }


    @Override
    public void execute(ReceiveCouponCmd cmd) {

        if (cmd.isHandleCode()) {
            log.info("handler处理兑换码");
            handleReceiveByCode(cmd);
        } else {
            log.info("handler处理优惠券id");
            handleReceiveById(cmd);
        }

    }


    void handleReceiveByCode(ReceiveCouponCmd cmd) {
        ExchangeCodeEntity code = codeDomainService.getExchangeCodeEntityByCode(cmd.getCode(), CodeType.COUPON);
        CouponEntity coupon = couponDomainService.getCouponEntity(code.getExchangeTargetId());

        cmd.setCouponId(coupon.getId()).setSerialNum(code.getId());
        handleReceiveById(cmd);
    }

    /**
     * 根据优惠券id领取优惠券
     */
    void handleReceiveById(ReceiveCouponCmd cmd) {
        CouponEntity couponEntity = couponDomainService.getCouponEntity(cmd.getCouponId());

        // 处理单个用户的高并发，防止刷量领取
        // todo 如果有很用户的话，每一个用户id都会存储到JVM字符串常量池中，导致内存溢出
        synchronized (cmd.getUserId().toString().intern()) {
            Map<Long, CouponReceive> userCouponMap = userCouponDomainService.userReceiveCouponNum(cmd.getUserId(), List.of(cmd.getCouponId()));

            // 优惠券需要判断处于发放中
            if (couponEntity.getStatus() != CouponStatus.ISSUING) {
                throw new BadRequestException("优惠券已经停止发放");
            }

            if (couponEntity.getIssueNum() >= couponEntity.getTotalNum()) {
                throw new AppCommonException("优惠券已经发完");
            }

            if (userCouponMap.get(cmd.getCouponId()).getCount() >= couponEntity.getUserLimit()) {
                throw new BadRequestException("优惠券已经超过个人领取限额");
            }

            UserCouponEntity userCouponEntity = new UserCouponEntity()
                    .setUserId(cmd.getUserId())
                    .setCouponId(cmd.getCouponId())
                    .setTermBeginTime(couponEntity.getTermBeginTime())
                    .setTermEndTime(couponEntity.getTermEndTime());

            /**
             * todo 注意锁的边界和事务的边界，
             * 因为事务是隔离的，如果事务在锁的前面，其中一个线程A释放锁了，此时该线程的事务还没有提交
             * 另外一个线程B此时拿到了锁，线程B会查询数据库,由于此时线程A的事务还没有提交，事务隔离的特性
             * 此时线程B查询的数据仍然是线程A最开始拿到的数据。这样会导致超领取。
             */
            //self.doReceiveCoupon(userCouponEntity);
            ((ReceiveCouponCmdHandler) AopContext.currentProxy()).doReceiveCoupon(cmd,userCouponEntity);
        }
    }

    @Transactional
    public void doReceiveCoupon(ReceiveCouponCmd cmd,UserCouponEntity userCouponEntity) {
        // 乐观锁来控制高并发，超领取的问题
        couponDomainService.incrementIssueNum(userCouponEntity.getCouponId());
        userCouponDomainService.receiveCoupon(userCouponEntity);
        if(cmd.isHandleCode()){
            codeDomainService.useExchangeCode(cmd.getSerialNum(), cmd.getUserId());
        }
    }
}



